package sg.edu.nus.iss.jass.datastore.impl;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import sg.edu.nus.iss.jass.datastore.AbstractDataStore;
import sg.edu.nus.iss.jass.datastore.AbstractFileStorage;
import sg.edu.nus.iss.jass.datastore.BaseDTO;
import sg.edu.nus.iss.jass.datastore.IDataStore;
import sg.edu.nus.iss.jass.datastore.MessageConstants;
import sg.edu.nus.iss.jass.datastore.Transaction;
import sg.edu.nus.iss.jass.datastore.exception.DataStoreException;
import sg.edu.nus.iss.jass.datastore.util.DataStoreHelper;

/**
 * <p>An implementation of transaction supported data store.</p>
 * 
 * <li>Instant flush data to storage mediation is closed when transaction started.</li>
 * </br>
 * 
 * The transaction can't handle the case when encountering System IO error,
 * for example, when program is running, the file format is changed by
 * third program, or deleted by third program</br>
 * 
 * Also see {@link AbstractFileStorage}, {@link IDataStore}, {@link Transaction}.
 * 
 * @author WANG DAYONG
 *
 */
public class TSupportFileDataStore extends AbstractDataStore implements
		Transaction {

	/**
	 * Container to keep data change information during transaction
	 */
	private Map<String, String> session;
	
	/**
	 * Unique id to identify a transaction
	 */
	private String transactionId;

	/**
	 * To indicate whether the current operation is in transaction
	 */
	private boolean inTransaction = false;

	/**
	 * To temporarily store the original flush indicator
	 */
	private boolean origFlushImmidiately = false;

	/**
	 * Constructor
	 * @param storage Concrete class of {@link AbstractFileStorage}
	 */
	public TSupportFileDataStore(AbstractFileStorage storage) {

		super(storage);
	}

	
	@Override
	public Transaction getTransaction() throws DataStoreException {
		this.transactionId = System.currentTimeMillis() +"_"+Thread.currentThread().getId();
		return (Transaction) this;
	}

	@Override
	public void startTransaction() throws DataStoreException {

		if (inTransaction) {
			throw new DataStoreException(MessageConstants.NESTED_TRAN_ERR);
		}
		inTransaction = true;
		origFlushImmidiately = storage.isInstantFlush();

		// in transaction model, if cache is on, always manipulate in memory
		storage.setInstantFlush(false);

		session = new HashMap<String, String>();

	}

	@Override
	public void commit() throws DataStoreException {

		try {

			for (Map.Entry<String, String> entry : session.entrySet()) {
				storage.flush(entry.getValue());
			}
			// set instant flush to true to tell storage layer that all the
			// shadow files
			// have written to HDD, then can do copy and delete operation on the
			// shadow file
			storage.setInstantFlush(true);
			for (Map.Entry<String, String> entry : session.entrySet()) {
				storage.copyFile(entry.getValue(), entry.getKey());
				storage.deleteFile(entry.getValue());
			}
		} catch (IOException e) {
			throw new DataStoreException(MessageConstants.TRAN_COMMIT_ERR, e);
		} finally {
			inTransaction = false;
			storage.setInstantFlush(origFlushImmidiately);
		}
	}

	@Override
	public void rollback() throws DataStoreException {

		try {
			//delete shadow content, in cache or system files
			for (Map.Entry<String, String> entry : session.entrySet()) {

				storage.deleteFile(entry.getValue());

			}
		} catch (IOException e) {
			throw new DataStoreException(MessageConstants.TRAN_ROLLBACK_ERR, e);
		} finally {
			inTransaction = false;
			storage.setInstantFlush(origFlushImmidiately);
		}

	}

	@SuppressWarnings("unchecked")
	@Override
	protected <T extends BaseDTO> List<T> loadAll(Type type) throws DataStoreException {

		if (inTransaction) {
			String className = DataStoreHelper.getClazzName((Class<?>) type);
			String copy = getShadowFileName(className);
			try {
				//if the operation is in transaction, and has modified data in transaction, the program 
				//will read data from shadow copy, to keep read consistency,
				//else program will read the original content 
				if (session.containsKey(className)) {
					return (List<T>) storage.load(copy);
				} else {
					return (List<T>) storage.load(className);
				}
			} catch (Exception ex) {
				throw new DataStoreException(MessageConstants.LOAD_ERR, ex);
			}
		} else {
			//if not in transaction, normal loading used
			return super.loadAll(type);
		}
	}

	@Override
	protected void saveAll(Type type, List<? extends BaseDTO> saveList) throws DataStoreException {

		if (inTransaction) {
			String className = DataStoreHelper.getClazzName((Class<?>) type);
			String copy = getShadowFileName(className);
			try {
				//if the operation is first on one type of object,
				//will create the shadow copy, and save changes in
				//shadow copy.
				//else the operation is not the first time on one type
				//of object, system will manipulate on shadow copy directly
				if (!session.containsKey(className)) {
					session.put(className, copy);
					storage.copyFile(className, copy);
					storage.save(copy, saveList);
				} else {
					storage.save(copy, saveList);
				}
			} catch (IOException e) {
				throw new DataStoreException(MessageConstants.SAVE_ERR, e);
			}
		} else {
			super.saveAll(type, saveList);
		}
	}
	
	public String getTransactionId(){
		return this.transactionId;
	}
	
	/**
	 * <p>Get the shadow file name.</p>
	 * @param fileName
	 * @return the shadow file name of given file name
	 */
	private String getShadowFileName(String fileName){
		String shadowName = null;
		if(fileName.indexOf(".") >= 0){
			shadowName = fileName.substring(fileName.lastIndexOf(".")+1).concat(getTransactionId());
		} else {
			shadowName = fileName.concat(getTransactionId());
		}
		return shadowName;
	}

	/**
	 * this is just for testing to clear all cache
	 */
	@Deprecated
	public void clearCache() {

		storage.clearCache();
	}
}
